<template>
  <div class="video-chat">
    <div v-if="isRoomEmpty">
      <p>{{ roomStatusText }}</p>
    </div>
    <!-- 视频双端显示 -->
    <div class="video_box">
      <div class="self_video">
        <div class="text_tip">我：<span class="userId">{{ userId }}</span></div>
        <video ref="localVideo" autoplay playsinline></video>
      </div>
      <div class="remote_video">
        <div class="text_tip">对方：<span class="userId">{{ oppositeUserId }}</span></div>
        <video ref="remoteVideo" autoplay playsinline></video>
      </div>
    </div>
    <!-- 加入房间按钮 -->
    <div class="room-controls">
      <div class="room-input">
        <input v-model="roomId" placeholder="请输入房间号" />
        <button @click="createRoom">创建房间</button>
        <button @click="joinRoomWithId">加入房间</button>
      </div>
      <div class="media-controls">
        <button @click="toggleAudio">
          {{ isAudioEnabled ? '关闭麦克风' : '打开麦克风' }}
        </button>
        <button @click="toggleVideo">
          {{ isVideoEnabled ? '关闭摄像头' : '打开摄像头' }}
        </button>
      </div>
    </div>
    <!-- 日志打印 -->
    <div class="log_box">
      <pre>
          <div v-for="(item, index) of logData" :key="index">{{ item }}</div>
        </pre>
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted, nextTick } from "vue";
import axios from "axios";

// WebRTC 相关变量
const localVideo = ref(null);
const remoteVideo = ref(null);
const isRoomEmpty = ref(true); // 判断房间是否为空

let localStream; // 本地流数据
let peerConnection; // RTC连接对象
let signalingSocket; // 信令服务器socket对象
let userId; // 当前用户ID
let oppositeUserId; // 对方用户ID

let logData = ref(["日志初始化..."]);

// 请求根路径，如果需要部署服务器，把对应ip改成自己服务器ip
let BaseUrl = "http://localhost:8095"

let wsUrl = "wss://localhost:8095";


// candidate信息
let candidateInfo = "";

// 发起端标识
let offerFlag = false;

// 房间状态文本
let roomStatusText = ref("点击'加入房间'开始音视频聊天");

// STUN 服务器，
// const iceServers = [
//   {
//     urls: "stun:stun.l.google.com:19302"  // Google 的 STUN 服务器
//   },
//   {
//     urls: "stun:自己的公网IP:3478" // 自己的Stun服务器
//   },
//   {
//     urls: "turn:自己的公网IP:3478",   // 自己的 TURN 服务器
//     username: "maohe",
//     credential: "maohe"
//   }
// ];
// ============< 看这 >================
// 没有搭建STUN和TURN服务器的使用如下ice配置即可
const iceServers = [
  {
    urls: "stun:stun.l.google.com:19302"  // Google 的 STUN 服务器
  }
];

// 在 script setup 中添加新的变量声明
const roomId = ref(''); // 房间号
const isAudioEnabled = ref(true); // 音频状态
const isVideoEnabled = ref(true); // 视频状态

onMounted(() => {
  generateRandomId();
})

// 加入房间，开启本地摄像头获取音视频流数据。
function joinRoomHandle() {
  roomStatusText.value = "等待对方加入房间..."
  getVideoStream();
}


// 获取本地视频 模拟从本地摄像头获取音视频流数据
async function getVideoStream() {
  try {
    localStream = await navigator.mediaDevices.getUserMedia({
      video: true,
      audio: true
    });
    localVideo.value.srcObject = localStream;
    wlog(`获取本地流成功~`)
    createPeerConnection(); // 创建RTC对象，监听candidate
  } catch (err) {
    console.error('获取本地媒体流失败:', err);
  }
}


// 初始化 WebSocket 连接
function initWebSocket() {
  wlog("开始连接websocket")
  // 连接ws时携带用户ID和房间号
  signalingSocket = new WebSocket(`${wsUrl}/rtc?userId=${userId}&roomId=${roomId.value}`);

  signalingSocket.onopen = () => {
    wlog('WebSocket 已连接');
  };

  // 消息处理
  signalingSocket.onmessage = (event) => {
    handleSignalingMessage(event.data);
  };
};

// 消息处理器 - 解析器
function handleSignalingMessage(message) {
  wlog("收到ws消息，开始解析...")
  wlog(message)
  let parseMsg = JSON.parse(message);
  wlog(`解析结果：${parseMsg}`);

  if (parseMsg.type == "join") {
    joinHandle(parseMsg.data);
  } else if (parseMsg.type == "offer") {
    wlog("收到发起端offer，开始解析...");
    offerHandle(parseMsg.data);
  } else if (parseMsg.type == "answer") {
    wlog("收到接收端的answer，开始解析...");
    answerHandle(parseMsg.data);
  }else if(parseMsg.type == "candidate"){
    wlog("收到远端candidate，开始解析...");
    candidateHandle(parseMsg.data);
  }

}

// 远端Candidate处理器
async function candidateHandle(candidate){
  peerConnection.addIceCandidate(new RTCIceCandidate(JSON.parse(candidate)));
  wlog("+++++++ 本端candidate设置完毕 ++++++++");
}

// 接收端的answer处理
async function answerHandle(answer) {
  wlog("将answer设置为远端信息");
  peerConnection.setRemoteDescription(new RTCSessionDescription(JSON.parse(answer))); // 设置远端SDP
}

// 发起端offer处理器
async function offerHandle(offer) {
  wlog("将发起端的offer设置为远端媒体信息");
  await peerConnection.setRemoteDescription(new RTCSessionDescription(JSON.parse(offer)));
  wlog("创建Answer 并设置到本地");
  let answer = await peerConnection.createAnswer()
  await peerConnection.setLocalDescription(answer);

  wlog("发送answer给发起端");
  // 构造answer消息发送给对端
  let paramObj = {
    userId: oppositeUserId,
    type: "answer",
    data: JSON.stringify(answer)
  }
  // 执行发送
  const res = await axios.post(`${BaseUrl}/rtcs/sendMessage`, paramObj);
}

// 加入处理器
function joinHandle(userIds) {
  // 判断连接的用户个数
  if (userIds.length == 1 && userIds[0] == userId) {
    wlog("标识为发起端，等待对方加入房间...")
    isRoomEmpty.value = true;
    // 存在一个连接并且是自身，标识我们是发起端
    offerFlag = true;
  } else if (userIds.length > 1) {
    // 对方加入了
    wlog("对方已连接...")
    isRoomEmpty.value = false;

    // 取出对方ID
    for (let id of userIds) {
      if (id != userId) {
        oppositeUserId = id;
      }
    }

    wlog(`对端ID: ${oppositeUserId}`)
    // 开始交换SDP和Candidate
    swapVideoInfo()
  }
}

// 交换SDP和candidate
async function swapVideoInfo() {
  wlog("开始交换Sdp和Candidate...");
  // 检查是否为发起端，如果是创建offer设置到本地，并发送给远端
  if (offerFlag) {
    wlog(`发起端创建offer`)
    let offer = await peerConnection.createOffer()
    await peerConnection.setLocalDescription(offer); // 将媒体信息设置到本地
    wlog("发启端设置SDP-offer到本地");

    // 构造消息ws发送给远端
    let paramObj = {
      userId: oppositeUserId,
      type: "offer",
      data: JSON.stringify(offer)
    };

    wlog(`构造offer信息发送给远端：${paramObj}`)

    // 执行发送
    const res = await axios.post(`${BaseUrl}/rtcs/sendMessage`, paramObj);

  }
}

// 将candidate信息发送给远端
async function sendCandidate(candidate) {
  // 构造消息ws发送给远端
  let paramObj = {
    userId: oppositeUserId,
    type: "candidate",
    data: JSON.stringify(candidate)
  };

  wlog(`构造candidate信息发送给远端：${paramObj}`);

  // 执行发送
  const res = await axios.post(`${BaseUrl}/rtcs/sendMessage`, paramObj);

}

// 创建RTC连接对象并监听和获取condidate信息
function createPeerConnection() {
  wlog("开始创建PC对象...")
  peerConnection = new RTCPeerConnection(iceServers);
  wlog("创建PC对象成功")
  // 创建RTC连接对象后连接websocket
  initWebSocket();

  // 监听网络信息（ICE Candidate）
  peerConnection.onicecandidate = (event) => {
    if (event.candidate) {
      candidateInfo = event.candidate;
      wlog("candidate信息变化...");
      // 将candidate信息发送给远端
      setTimeout(()=>{
        sendCandidate(event.candidate);
      }, 150)
    }
  };

  // 监听远端音视频流
  peerConnection.ontrack = (event) => {
    nextTick(() => {
      wlog("====> 收到远端数据流 <=====")
      if (!remoteVideo.value.srcObject) {
        remoteVideo.value.srcObject = event.streams[0];
        remoteVideo.value.play();  // 强制播放
      }
    });
  };

  // 监听ice连接状态
  peerConnection.oniceconnectionstatechange = () => {
    wlog(`RTC连接状态改变：${peerConnection.iceConnectionState}`);
  };


  // 添加本地音视频流到 PeerConnection
  localStream.getTracks().forEach(track => {
    peerConnection.addTrack(track, localStream);
  });
}


// 日志编写
function wlog(text) {
  logData.value.unshift(text);
}

// 给用户生成随机ID.
function generateRandomId() {
  userId = Math.random().toString(36).substring(2, 12); // 生成10位的随机ID
  wlog(`分配到ID:${userId}`)
}

// 创建房间
async function createRoom() {
  if (!roomId.value) {
    alert('请输入房间号');
    return;
  }
  try {
    const res = await axios.post(`${BaseUrl}/rtcs/createRoom`, {
      roomId: roomId.value,
      userId: userId
    });
    if (res.data.success) {
      wlog(`创建房间成功：${roomId.value}`);
      joinRoomHandle();
    }
  } catch (error) {
    wlog(`创建房间失败：${error}`);
  }
}

// 加入指定房间
async function joinRoomWithId() {
  if (!roomId.value) {
    alert('请输入房间号');
    return;
  }
  try {
    const res = await axios.post(`${BaseUrl}/rtcs/joinRoom`, {
      roomId: roomId.value,
      userId: userId
    });
    if (res.data.success) {
      wlog(`加入房间成功：${roomId.value}`);
      joinRoomHandle();
    }
  } catch (error) {
    wlog(`加入房间失败：${error}`);
  }
}

// 切换音频
function toggleAudio() {
  if (localStream) {
    const audioTrack = localStream.getAudioTracks()[0];
    if (audioTrack) {
      audioTrack.enabled = !audioTrack.enabled;
      isAudioEnabled.value = audioTrack.enabled;
      wlog(`麦克风已${audioTrack.enabled ? '打开' : '关闭'}`);
    }
  }
}

// 切换视频
function toggleVideo() {
  if (localStream) {
    const videoTrack = localStream.getVideoTracks()[0];
    if (videoTrack) {
      videoTrack.enabled = !videoTrack.enabled;
      isVideoEnabled.value = videoTrack.enabled;
      wlog(`摄像头已${videoTrack.enabled ? '打开' : '关闭'}`);
    }
  }
}
</script>
<style scoped>
.video-chat {
  display: flex;
  flex-direction: column;
  align-items: center;
}

video {
  width: 300px;
  height: 200px;
  margin: 10px;
}

.remote_video {
  border: solid rgb(30, 40, 226) 1px;
  margin-left: 20px;
}

.self_video {
  border: solid red 1px;
}

.video_box {
  display: flex;
}

.video_box div {
  border-radius: 10px;
}

.join_room_btn button {
  border: none;
  background-color: rgb(119 178 63);
  height: 30px;
  width: 80px;
  border-radius: 10px;
  color: white;
  margin-top: 10px;
  cursor: pointer;
  font-size: 13px;
}

.text_tip {
  font-size: 13px;
  color: #484848;
  padding: 6px;
}

pre {
  width: 600px;
  height: 300px;
  background-color: #d4d4d4;
  border-radius: 10px;
  padding: 10px;
  overflow-y: auto;
}

pre div {
  padding: 4px 0px;
  font-size: 15px;
}

.userId{
  color: #3669ad;
}

.video-chat p{
  font-weight: 600;
  color: #b24242;
}

.room-controls {
  margin: 20px 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.room-input {
  display: flex;
  gap: 10px;
  align-items: center;
}

.room-input input {
  padding: 5px 10px;
  border: 1px solid #ccc;
  border-radius: 5px;
}

.media-controls {
  display: flex;
  gap: 10px;
}

.room-controls button {
  border: none;
  background-color: rgb(119 178 63);
  height: 30px;
  padding: 0 15px;
  border-radius: 5px;
  color: white;
  cursor: pointer;
  font-size: 13px;
}

.media-controls button {
  background-color: #3669ad;
}
</style>

